@@ -0,0 +1,21 @@ |
||
1 |
+class ScenarioImportsController < ApplicationController |
|
2 |
+ def new |
|
3 |
+ @scenario_import = ScenarioImport.new |
|
4 |
+ end |
|
5 |
+ |
|
6 |
+ def create |
|
7 |
+ @scenario_import = ScenarioImport.new(params[:scenario_import]) |
|
8 |
+ @scenario_import.set_user(current_user) |
|
9 |
+ |
|
10 |
+ if @scenario_import.valid? |
|
11 |
+ if @scenario_import.do_import? |
|
12 |
+ @scenario_import.import! |
|
13 |
+ redirect_to @scenario_import.scenario, notice: "Import successful!" |
|
14 |
+ else |
|
15 |
+ render action: "new" |
|
16 |
+ end |
|
17 |
+ else |
|
18 |
+ render action: "new" |
|
19 |
+ end |
|
20 |
+ end |
|
21 |
+end |
@@ -0,0 +1,80 @@ |
||
1 |
+# This is a helper class for managing Scenario imports. |
|
2 |
+class ScenarioImport |
|
3 |
+ include ActiveModel::Model |
|
4 |
+ include ActiveModel::Callbacks |
|
5 |
+ include ActiveModel::Validations::Callbacks |
|
6 |
+ |
|
7 |
+ URL_REGEX = /\Ahttps?:\/\//i |
|
8 |
+ |
|
9 |
+ attr_accessor :file, :url, :data, :do_import |
|
10 |
+ |
|
11 |
+ attr_reader :user |
|
12 |
+ |
|
13 |
+ before_validation :fetch_url |
|
14 |
+ before_validation :parse_file |
|
15 |
+ |
|
16 |
+ validate :validate_presence_of_file_url_or_data |
|
17 |
+ validates_format_of :url, :with => URL_REGEX, :allow_nil => true, :allow_blank => true, :message => "appears to be invalid" |
|
18 |
+ validate :validate_data |
|
19 |
+ |
|
20 |
+ def step_one? |
|
21 |
+ data.blank? |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ def step_two? |
|
25 |
+ valid? |
|
26 |
+ end |
|
27 |
+ |
|
28 |
+ def set_user(user) |
|
29 |
+ @user = user |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ def existing_scenario |
|
33 |
+ @existing_scenario ||= user.scenarios.find_by_guid(parsed_data["guid"]) |
|
34 |
+ end |
|
35 |
+ |
|
36 |
+ def parsed_data |
|
37 |
+ @parsed_data |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ def do_import? |
|
41 |
+ do_import == "1" |
|
42 |
+ end |
|
43 |
+ |
|
44 |
+ def import! |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ def scenario |
|
48 |
+ existing_scenario |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ protected |
|
52 |
+ |
|
53 |
+ def parse_file |
|
54 |
+ if data.blank? && file.present? |
|
55 |
+ self.data = file.read |
|
56 |
+ end |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ def fetch_url |
|
60 |
+ if data.blank? && url.present? && url =~ URL_REGEX |
|
61 |
+ self.data = Faraday.get(url).body |
|
62 |
+ end |
|
63 |
+ end |
|
64 |
+ |
|
65 |
+ def validate_data |
|
66 |
+ if data.present? |
|
67 |
+ @parsed_data = JSON.parse(data) rescue {} |
|
68 |
+ if (%w[name guid] - @parsed_data.keys).length > 0 |
|
69 |
+ errors.add(:base, "The provided data does not appear to be a valid Scenario.") |
|
70 |
+ self.data = nil |
|
71 |
+ end |
|
72 |
+ end |
|
73 |
+ end |
|
74 |
+ |
|
75 |
+ def validate_presence_of_file_url_or_data |
|
76 |
+ unless file.present? || url.present? || data.present? |
|
77 |
+ errors.add(:base, "Please provide either a Scenario JSON File or a Public Scenario URL.") |
|
78 |
+ end |
|
79 |
+ end |
|
80 |
+end |
@@ -0,0 +1,23 @@ |
||
1 |
+<div class="page-header"> |
|
2 |
+ <h2> |
|
3 |
+ Import a Public Scenario |
|
4 |
+ </h2> |
|
5 |
+</div> |
|
6 |
+ |
|
7 |
+<blockquote>You can import Scenarios, either from a <code>.json</code> file, or via a public Scenario URL. When you import a Scenario, Huginn will keep track of where it came from and later let you update it.</blockquote> |
|
8 |
+ |
|
9 |
+<div class="col-md-4"> |
|
10 |
+ <div class="form-group"> |
|
11 |
+ <%= f.label :url, 'Option 1: Provide a Public Scenario URL' %> |
|
12 |
+ <%= f.text_field :url, :class => 'form-control', :placeholder => "Public Scenario URL" %> |
|
13 |
+ </div> |
|
14 |
+ |
|
15 |
+ <div class="form-group"> |
|
16 |
+ <%= f.label :file, 'Option 2: Upload a Scenario JSON File' %> |
|
17 |
+ <%= f.file_field :file, :class => 'form-control' %> |
|
18 |
+ </div> |
|
19 |
+ |
|
20 |
+ <div class='form-actions'> |
|
21 |
+ <%= f.submit "Start Import", :class => "btn btn-primary" %> |
|
22 |
+ </div> |
|
23 |
+</div> |
@@ -0,0 +1,51 @@ |
||
1 |
+<div class="col-md-12"> |
|
2 |
+ <div class="page-header"> |
|
3 |
+ <h2><%= @scenario_import.parsed_data["name"] %> (exported <%= time_ago_in_words Time.parse(@scenario_import.parsed_data["exported_at"]) %> ago)</h2> |
|
4 |
+ </div> |
|
5 |
+ |
|
6 |
+ <% if @scenario_import.parsed_data["description"].present? %> |
|
7 |
+ <blockquote><%= @scenario_import.parsed_data["description"] %></blockquote> |
|
8 |
+ <% end %> |
|
9 |
+ |
|
10 |
+ <p> |
|
11 |
+ This import contains <%= pluralize @scenario_import.parsed_data["agents"].length, "Agent" %>: |
|
12 |
+ </p> |
|
13 |
+ |
|
14 |
+ <ul class='agent-import-list'> |
|
15 |
+ <% @scenario_import.parsed_data["agents"].each do |agent_data| %> |
|
16 |
+ <li> |
|
17 |
+ <%= link_to agent_data['name'], '#', :class => 'options-toggle' %> |
|
18 |
+ <span class='text-muted'> |
|
19 |
+ (<%= agent_data["type"].split("::").pop.titleize %>) |
|
20 |
+ </span> |
|
21 |
+ <pre class='options' style='display: none;'><%= Utils.pretty_jsonify agent_data["options"] || {} %></pre> |
|
22 |
+ </li> |
|
23 |
+ <% end %> |
|
24 |
+ </ul> |
|
25 |
+ |
|
26 |
+ <script> |
|
27 |
+ $(function() { |
|
28 |
+ $('.agent-import-list .options-toggle').on('click', function(e) { |
|
29 |
+ e.preventDefault(); |
|
30 |
+ $(this).siblings('.options').fadeToggle(); |
|
31 |
+ }); |
|
32 |
+ }); |
|
33 |
+ </script> |
|
34 |
+ |
|
35 |
+ <% if @scenario_import.existing_scenario.present? %> |
|
36 |
+ <strong> |
|
37 |
+ This Scenario already exists on your Huginn. |
|
38 |
+ If you continue, the import will overwrite your existing <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario. |
|
39 |
+ </strong> |
|
40 |
+ <% end %> |
|
41 |
+ |
|
42 |
+ <div class="checkbox"> |
|
43 |
+ <%= f.label :do_import do %> |
|
44 |
+ <%= f.check_box :do_import %> I confirm that I want to import these Agents. |
|
45 |
+ <% end %> |
|
46 |
+ </div> |
|
47 |
+ |
|
48 |
+ <div class='form-actions'> |
|
49 |
+ <%= f.submit "Finish Import", :class => "btn btn-primary" %> |
|
50 |
+ </div> |
|
51 |
+</div> |
@@ -0,0 +1,34 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <% if @scenario_import.errors.any? %> |
|
5 |
+ <div class="row well"> |
|
6 |
+ <h2><%= pluralize(@scenario_import.errors.count, "error") %> prohibited this Scenario from being imported:</h2> |
|
7 |
+ <% @scenario_import.errors.full_messages.each do |msg| %> |
|
8 |
+ <p class='text-warning'><%= msg %></p> |
|
9 |
+ <% end %> |
|
10 |
+ </div> |
|
11 |
+ <% end %> |
|
12 |
+ |
|
13 |
+ <%= form_for @scenario_import, :multipart => true do |f| %> |
|
14 |
+ <%= f.hidden_field :data %> |
|
15 |
+ |
|
16 |
+ <div class="row"> |
|
17 |
+ <% if @scenario_import.step_one? %> |
|
18 |
+ <%= render 'step_one', :f => f %> |
|
19 |
+ <% elsif @scenario_import.step_two? %> |
|
20 |
+ <%= render 'step_two', :f => f %> |
|
21 |
+ <% end %> |
|
22 |
+ </div> |
|
23 |
+ <% end %> |
|
24 |
+ |
|
25 |
+ <hr /> |
|
26 |
+ |
|
27 |
+ <div class="row"> |
|
28 |
+ <div class="col-md-12"> |
|
29 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
30 |
+ </div> |
|
31 |
+ </div> |
|
32 |
+ </div> |
|
33 |
+ </div> |
|
34 |
+</div> |
@@ -7,9 +7,7 @@ |
||
7 | 7 |
</h2> |
8 | 8 |
</div> |
9 | 9 |
|
10 |
- <blockquote> |
|
11 |
- Scenarios are named groups of Agents. Scenarios allow you to organize your agents, and to export sets of Agents for sharing. |
|
12 |
- </blockquote> |
|
10 |
+ <blockquote>Scenarios are named groups of Agents. Scenarios allow you to organize your agents, and to export sets of Agents for sharing.</blockquote> |
|
13 | 11 |
|
14 | 12 |
<table class='table table-striped'> |
15 | 13 |
<tr> |
@@ -42,6 +40,7 @@ |
||
42 | 40 |
|
43 | 41 |
<div class="btn-group"> |
44 | 42 |
<%= link_to '<span class="glyphicon glyphicon-plus"></span> New Scenario'.html_safe, new_scenario_path, class: "btn btn-default" %> |
43 |
+ <%= link_to '<span class="glyphicon glyphicon-plus"></span> Import Scenario'.html_safe, new_scenario_imports_path, class: "btn btn-default" %> |
|
45 | 44 |
</div> |
46 | 45 |
</div> |
47 | 46 |
</div> |
@@ -28,6 +28,10 @@ Huginn::Application.routes.draw do |
||
28 | 28 |
end |
29 | 29 |
|
30 | 30 |
resources :scenarios do |
31 |
+ collection do |
|
32 |
+ resource :scenario_imports, :only => [:new, :create] |
|
33 |
+ end |
|
34 |
+ |
|
31 | 35 |
member do |
32 | 36 |
get :share |
33 | 37 |
get :export |
@@ -0,0 +1,7 @@ |
||
1 |
+class AddIndicesToScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_index :scenarios, [:user_id, :guid] |
|
4 |
+ add_index :scenario_memberships, :agent_id |
|
5 |
+ add_index :scenario_memberships, :scenario_id |
|
6 |
+ end |
|
7 |
+end |
@@ -11,7 +11,7 @@ |
||
11 | 11 |
# |
12 | 12 |
# It's strongly recommended that you check this file into your version control system. |
13 | 13 |
|
14 |
-ActiveRecord::Schema.define(version: 20140531232016) do |
|
14 |
+ActiveRecord::Schema.define(version: 20140602014917) do |
|
15 | 15 |
|
16 | 16 |
create_table "agent_logs", force: true do |t| |
17 | 17 |
t.integer "agent_id", null: false |
@@ -97,6 +97,9 @@ ActiveRecord::Schema.define(version: 20140531232016) do |
||
97 | 97 |
t.datetime "updated_at" |
98 | 98 |
end |
99 | 99 |
|
100 |
+ add_index "scenario_memberships", ["agent_id"], name: "index_scenario_memberships_on_agent_id", using: :btree |
|
101 |
+ add_index "scenario_memberships", ["scenario_id"], name: "index_scenario_memberships_on_scenario_id", using: :btree |
|
102 |
+ |
|
100 | 103 |
create_table "scenarios", force: true do |t| |
101 | 104 |
t.string "name", null: false |
102 | 105 |
t.integer "user_id", null: false |
@@ -108,6 +111,8 @@ ActiveRecord::Schema.define(version: 20140531232016) do |
||
108 | 111 |
t.string "source_url" |
109 | 112 |
end |
110 | 113 |
|
114 |
+ add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", using: :btree |
|
115 |
+ |
|
111 | 116 |
create_table "user_credentials", force: true do |t| |
112 | 117 |
t.integer "user_id", null: false |
113 | 118 |
t.string "credential_name", null: false |
@@ -0,0 +1,29 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenarioImportsController do |
|
4 |
+ def valid_attributes(options = {}) |
|
5 |
+ { :name => "some_name" }.merge(options) |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ before do |
|
9 |
+ sign_in users(:bob) |
|
10 |
+ end |
|
11 |
+ |
|
12 |
+ describe "GET new" do |
|
13 |
+ it "initializes a new ScenarioImport and renders new" do |
|
14 |
+ get :new |
|
15 |
+ assigns(:scenario_import).should be_a(ScenarioImport) |
|
16 |
+ response.should render_template(:new) |
|
17 |
+ end |
|
18 |
+ end |
|
19 |
+ |
|
20 |
+ describe "POST create" do |
|
21 |
+ it "initializes a ScenarioImport for current_user, passing in params" do |
|
22 |
+ post :create, :scenario_import => { :url => "bad url" } |
|
23 |
+ assigns(:scenario_import).user.should == users(:bob) |
|
24 |
+ assigns(:scenario_import).url.should == "bad url" |
|
25 |
+ response.should render_template(:new) |
|
26 |
+ end |
|
27 |
+ end |
|
28 |
+end |
|
29 |
+ |
@@ -51,7 +51,8 @@ describe Agents::SlackAgent do |
||
51 | 51 |
username: @event.payload[:username] |
52 | 52 |
) |
53 | 53 |
end |
54 |
- expect(@checker.receive([@event])).to_not raise_error |
|
54 |
+ |
|
55 |
+ lambda { @checker.receive([@event]) }.should_not raise_error |
|
55 | 56 |
end |
56 | 57 |
end |
57 | 58 |
|
@@ -0,0 +1,80 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenarioImport do |
|
4 |
+ describe "initialization" do |
|
5 |
+ it "is initialized with an attributes hash" do |
|
6 |
+ ScenarioImport.new(:url => "http://google.com").url.should == "http://google.com" |
|
7 |
+ end |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ describe "validations" do |
|
11 |
+ subject { ScenarioImport.new } |
|
12 |
+ let(:valid_json) { { :name => "some scenario", :guid => "someguid" }.to_json } |
|
13 |
+ let(:invalid_json) { { :name => "some scenario missing a guid" }.to_json } |
|
14 |
+ |
|
15 |
+ it "is not valid when none of file, url, or data are present" do |
|
16 |
+ subject.should_not be_valid |
|
17 |
+ subject.should have(1).error_on(:base) |
|
18 |
+ subject.errors[:base].should include("Please provide either a Scenario JSON File or a Public Scenario URL.") |
|
19 |
+ end |
|
20 |
+ |
|
21 |
+ describe "data" do |
|
22 |
+ it "should be invalid with invalid data" do |
|
23 |
+ subject.data = invalid_json |
|
24 |
+ subject.should_not be_valid |
|
25 |
+ subject.should have(1).error_on(:base) |
|
26 |
+ |
|
27 |
+ subject.data = "foo" |
|
28 |
+ subject.should_not be_valid |
|
29 |
+ subject.should have(1).error_on(:base) |
|
30 |
+ |
|
31 |
+ # It also clears the data when invalid |
|
32 |
+ subject.data.should be_nil |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ it "should be valid with valid data" do |
|
36 |
+ subject.data = valid_json |
|
37 |
+ subject.should be_valid |
|
38 |
+ end |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ describe "url" do |
|
42 |
+ it "should be invalid with an unreasonable URL" do |
|
43 |
+ subject.url = "foo" |
|
44 |
+ subject.should_not be_valid |
|
45 |
+ subject.should have(1).error_on(:url) |
|
46 |
+ subject.errors[:url].should include("appears to be invalid") |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ it "should be invalid when the referenced url doesn't contain a scenario" do |
|
50 |
+ stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_json) |
|
51 |
+ subject.url = "http://example.com/scenarios/1/export.json" |
|
52 |
+ subject.should_not be_valid |
|
53 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ it "should be valid when the url points to a valid scenario" do |
|
57 |
+ stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_json) |
|
58 |
+ subject.url = "http://example.com/scenarios/1/export.json" |
|
59 |
+ subject.should be_valid |
|
60 |
+ end |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ describe "file" do |
|
64 |
+ it "should be invalid when the uploaded file doesn't contain a scenario" do |
|
65 |
+ subject.file = StringIO.new("foo") |
|
66 |
+ subject.should_not be_valid |
|
67 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
68 |
+ |
|
69 |
+ subject.file = StringIO.new(invalid_json) |
|
70 |
+ subject.should_not be_valid |
|
71 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
72 |
+ end |
|
73 |
+ |
|
74 |
+ it "should be valid with a valid uploaded scenario" do |
|
75 |
+ subject.file = StringIO.new(valid_json) |
|
76 |
+ subject.should be_valid |
|
77 |
+ end |
|
78 |
+ end |
|
79 |
+ end |
|
80 |
+end |